//! Functional helpers that mirror Lodash utilities such as `tap`, `thru` and `times`.
//! 函数式工具集合，对齐 Lodash 的 `tap`、`thru`、`times` 等行为。

use std::panic::{self, UnwindSafe};

/// Invokes `inspector` with a reference to `value`, returning the original value, like `_.tap`.
/// 类似 `_.tap`，在链式调用中窥视值但不改变其返回。
pub fn tap<T, F>(
    value: T,
    mut inspector: F,
) -> T
where
    F: FnMut(&T),
{
    inspector(&value);
    value
}

/// Passes the value through a transformer, mirroring `_.thru`.
/// 类似 `_.thru`，将值传入转换函数并返回结果。
pub fn thru<T, R, F>(
    value: T,
    mut transformer: F,
) -> R
where
    F: FnMut(T) -> R,
{
    transformer(value)
}

/// Executes `iteratee` `count` times, collecting the results, similar to `_.times`.
/// 类似 `_.times`，按次数执行闭包并收集结果。
pub fn times<T, F>(
    count: usize,
    mut iteratee: F,
) -> Vec<T>
where
    F: FnMut(usize) -> T,
{
    (0..count).map(|index| iteratee(index)).collect()
}

/// Executes a function, capturing panics as an error string, like `_.attempt`.
/// 类似 `_.attempt`，执行闭包并捕获 panic 信息返回。
pub fn attempt<T, F>(func: F) -> Result<T, String>
where
    F: FnOnce() -> T + UnwindSafe,
{
    match panic::catch_unwind(func) {
        Ok(value) => Ok(value),
        Err(payload) => Err(payload_to_string(payload)),
    }
}

fn payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
    match payload.downcast::<String>() {
        Ok(message) => *message,
        Err(payload) => match payload.downcast::<&'static str>() {
            Ok(message) => message.to_string(),
            Err(_) => "function panicked".to_string(),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tap_and_thru_follow_functional_style() {
        let inspected = tap(vec![1, 2, 3], |values| assert_eq!(values.len(), 3));
        assert_eq!(inspected, vec![1, 2, 3]);

        let doubled = thru(21, |value| value * 2);
        assert_eq!(doubled, 42);
    }

    #[test]
    fn times_and_attempt_match_lodash_contract() {
        assert_eq!(times(3, |i| i * i), vec![0, 1, 4]);

        let result = attempt(|| 1 + 1);
        assert_eq!(result.unwrap(), 2);

        let error = attempt(|| panic!("boom")).unwrap_err();
        assert_eq!(error, "boom");
    }
}
